/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import { Address } from "../Address"
import { RealCall } from "./RealCall"
import { RealConnectionPool } from "./RealConnectionPool"
import { EventListener } from '../EventListener'
import { Route } from '../Route'
import { HttpUrl } from "../HttpUrl"
import { RealInterceptorChain } from "../http/RealInterceptorChain"
import { OkHttpClient } from "../OkHttpClient"
import { ExchangeCodec } from "../http/ExchangeCodec"
import { IOException, RouteException } from "../Error/error"
import { RealConnection } from "./RealConnection"
import { RouteSelector, Selection } from './RouteSelector'
import { canReuseConnectionFor, kotlinCheck } from "../utils/Utils"
import { List } from "@kit.ArkTS"
import { StreamResetException, ConnectionShutdownException } from "../Error/error"
import { ErrorCode } from '../http2/ErrorCode'
import { socket } from "@kit.NetworkKit"

/**
 * Attempts to find the connections for an exchange and any retries that follow. This uses the
 * following strategies:
 *
 *  1. If the current call already has a connection that can satisfy the request it is used. Using
 *     the same connection for an initial exchange and its follow-ups may improve locality.
 *
 *  2. If there is a connection in the pool that can satisfy the request it is used. Note that it is
 *     possible for shared exchanges to make requests to different host names! See
 *     [RealConnection.isEligible] for details.
 *
 *  3. If there's no existing connection, make a list of routes (which may require blocking DNS
 *     lookups) and attempt a new connection them. When failures occur, retries iterate the list of
 *     available routes.
 *
 * If the pool gains an eligible connection while DNS, TCP, or TLS work is in flight, this finder
 * will prefer pooled connections. Only pooled HTTP/2 connections are used for such de-duplication.
 *
 * It is possible to cancel the finding process.
 *
 * Instances of this class are not thread-safe. Each instance is thread-confined to the thread
 * executing [call].
 */
export class ExchangeFinder{

  private connectionPool: RealConnectionPool
  address: Address
  private call: RealCall
  private eventListener: EventListener
  constructor(connectionPool: RealConnectionPool, address: Address, call: RealCall, eventListener: EventListener) {
    this.connectionPool = connectionPool
    this.address = address
    this.call = call
    this.eventListener = eventListener
  }
  private routeSelection?: Selection = null
  private routeSelector?: RouteSelector = null
  private refusedStreamCount = 0
  private connectionShutdownCount = 0
  private otherFailureCount = 0
  private nextRouteToTry: Route | null = null

  async find(client: OkHttpClient, chain: RealInterceptorChain): Promise<ExchangeCodec> {
    try {
      let resultConnection = await this.findHealthyConnection(
        chain.connectTimeoutMillis(),
        chain.readTimeoutMillis(),
        chain.writeTimeoutMillis(),
        client.pingIntervalMillis,
        client.retryOnConnectionFailure,
        chain.request().method !== "GET"
      )
      return resultConnection.newCodec(client, chain)
    } catch (e) {
      if (e instanceof RouteException) {
        this.trackFailure(e.lastConnectException)
        throw e
      } else if (e instanceof IOException) {
        this.trackFailure(e)
        throw new RouteException(e)
      } else {
        // 其他类型的异常，直接抛出
        throw e
      }
    }
  }

  /**
   * Finds a connection and returns it if it is healthy. If it is unhealthy the process is repeated
   * until a healthy connection is found.
   */
  // @Throws(IOException::class)
  private async findHealthyConnection(connectTimeout: number, readTimeout: number, writeTimeout: number, pingIntervalMillis: number, connectionRetryEnabled: boolean, doExtensiveHealthChecks: boolean): Promise<RealConnection> {
    while (true) {
      let candidate = await this.findConnection(
        connectTimeout = connectTimeout,
        readTimeout = readTimeout,
        writeTimeout = writeTimeout,
        pingIntervalMillis = pingIntervalMillis,
        connectionRetryEnabled = connectionRetryEnabled
      )

      // Confirm that the connection is good.
      if (await candidate.isHealthy(doExtensiveHealthChecks)) {
        console.log("okhttp test log :: " , "findHealthyConnection : 健康连接直接返回")
        return candidate
      }else{
        console.log("okhttp test log :: " , "findHealthyConnection : 不健康连接继续")
      }

      // If it isn't, take it out of the pool.
      candidate.noNewExchanges()

      // Make sure we have some routes left to try. One example where we may exhaust all the routes
      // would happen if we made a new connection and it immediately is detected as unhealthy.
      if (this.nextRouteToTry !== null && this.nextRouteToTry !== undefined) continue

      let routesLeft = this.routeSelection?.hasNext() ?? true
      if (routesLeft) continue

      let routesSelectionLeft = this.routeSelector?.hasNext() ?? true
      if (routesSelectionLeft) continue

      throw new IOException("exhausted all routes")
    }
  }

  /**
   * Returns a connection to host a new stream. This prefers the existing connection if it exists,
   * then the pool, finally building a new connection.
   *
   * This checks for cancellation before each blocking operation.
   */
  // @Throws(IOException::class)
  private async findConnection(connectTimeout: number, readTimeout: number, writeTimeout: number, pingIntervalMillis: number, connectionRetryEnabled: boolean): Promise<RealConnection> {
    if (this.call.isCanceled()) throw new IOException("Canceled")

    // 待删除注释标记:: 重试机制复用连接逻辑
    // Attempt to reuse the connection from the call.
    let callConnection = this.call.connection // This may be mutated by releaseConnectionNoEvents()!
    console.log("okhttp test log :: " , "findConnection : ", callConnection === null)

    if (callConnection !== null && callConnection !== undefined) {
      let toClose: socket.TCPSocket | socket.TLSSocket | null = null
      // synchronized(callConnection) {
      if (callConnection.getNoNewExchanges() || !this.sameHostAndPort(callConnection.route().address.url)) {
        toClose = this.call.releaseConnectionNoEvents()
      }
      // }

      // If the call's connection wasn't released, reuse it. We don't call connectionAcquired() here
      // because we already acquired it.
      if (this.call.connection !== null && this.call.connection !== undefined) {
        kotlinCheck(toClose === null || toClose === undefined,"")
        return callConnection
      }
      // The call's connection was released.
      console.log("okhttp test log :: " , "findConnection : toClose === null", toClose === null)

      // 关闭 socket 时可能抛出错误（如果 socket 已经被 on('close') 关闭），忽略这些错误
      try {
        await toClose?.close()
      } catch (closeError) {
        console.log("okhttp :: findConnection: ignoring error while closing released socket:", closeError)
      }
      this.eventListener.connectionReleased(this.call, callConnection)
    }

    // We need a new connection. Give it fresh stats.
    this.refusedStreamCount = 0
    this.connectionShutdownCount = 0
    this.otherFailureCount = 0

    // Attempt to get a connection from the pool.
    if (await this.connectionPool.callAcquirePooledConnection(this.address, this.call, null, false)) {
      console.log("okhttp test log :: " , "findConnection : 第一次匹配到socket")
      let result = this.call.connection
      this.eventListener.connectionAcquired(this.call, result)
      return result
    }

    // Nothing in the pool. Figure out what route we'll try next.
    let routes: List<Route> | null
    let route: Route
    if (this.nextRouteToTry !== null && this.nextRouteToTry !== undefined) {
      // Use a route from a preceding coalesced connection.
      routes = null
      route = this.nextRouteToTry!!
      this.nextRouteToTry = null
    } else if (this.routeSelection !== null && this.routeSelection !== undefined && this.routeSelection!!.hasNext()) {
      // Use a route from an existing route selection.
      routes = null
      route = this.routeSelection!!.next()
    } else {
      // Compute a new route selection. This is a blocking operation!
      let localRouteSelector = this.routeSelector
      if (localRouteSelector === null || localRouteSelector === undefined) {
        localRouteSelector = new RouteSelector(this.address, this.call.client.routeDatabase, this.call, this.eventListener)
        this.routeSelector = localRouteSelector
      }
      let localRouteSelection = await localRouteSelector.next()
      this.routeSelection = localRouteSelection
      routes = localRouteSelection.routes

      if (this.call.isCanceled()) throw new IOException("Canceled")

      // Now that we have a set of IP addresses, make another attempt at getting a connection from
      // the pool. We have a better chance of matching thanks to connection coalescing.
      if (await this.connectionPool.callAcquirePooledConnection(this.address, this.call, routes, false)) {
        console.log("okhttp test log :: " , "findConnection : 第二次匹配到socket")
        let result = this.call.connection
        this.eventListener.connectionAcquired(this.call, result)
        return result
      }

      route = localRouteSelection.next()
    }

    // Connect. Tell the call about the connecting call so async cancels work.
    let newConnection = new RealConnection(this.connectionPool, route)
    this.call.connectionToCancel = newConnection
    try {
      await newConnection.connect(
        connectTimeout,
        readTimeout,
        writeTimeout,
        pingIntervalMillis,
        connectionRetryEnabled,
        this.call,
        this.eventListener
      )
    } finally {
      this.call.connectionToCancel = null
    }
    this.call.client.routeDatabase.connected(newConnection.route())

    // If we raced another call connecting to this host, coalesce the connections. This makes for 3
    // different lookups in the connection pool!
    if (await this.connectionPool.callAcquirePooledConnection(this.address, this.call, routes, true)) {
      let result = this.call.connection!!
      this.nextRouteToTry = route
      await newConnection.socket?.close()
      this.eventListener.connectionAcquired(this.call, result)
      return result
    }

    console.log("okhttp test log :: " , "findConnection : 推入connectionPool")
    // synchronized(newConnection) {
    this.connectionPool.put(newConnection)
    console.log("okhttp test log :: " , "Socket.connectionPool length : ", this.connectionPool.connectionCount())
    this.call.acquireConnectionNoEvents(newConnection)
    // }

    this.eventListener.connectionAcquired(this.call, newConnection)
    return newConnection
  }

  trackFailure(e: Error) {
    this.nextRouteToTry = null
    if (e instanceof StreamResetException && e.errorCode === ErrorCode.REFUSED_STREAM) {
      this.refusedStreamCount++
    } else if (e instanceof ConnectionShutdownException) {
      this.connectionShutdownCount++
    } else {
      this.otherFailureCount++
    }
  }

  /**
   * Returns true if the current route has a failure that retrying could fix, and that there's
   * a route to retry on.
   */
  retryAfterFailure(): boolean {
    if (this.refusedStreamCount === 0 && this.connectionShutdownCount === 0 && this.otherFailureCount === 0) {
      return false // Nothing to recover from.
    }

    if (this.nextRouteToTry !== null && this.nextRouteToTry !== undefined) {
      return true
    }

    let retryRoute = this.retryRoute()
    if (retryRoute !== null && retryRoute !== undefined) {
      // Lock in the route because retryRoute() is racy and we don't want to call it twice.
      this.nextRouteToTry = retryRoute
      return true
    }

    // If we have a routes left, use 'em.
    if (this.routeSelection?.hasNext() == true) return true

    // If we haven't initialized the route selector yet, assume it'll have at least one route.
    let localRouteSelector = this.routeSelector
    if (localRouteSelector === null || localRouteSelector === undefined) return true

    // If we do have a route selector, use its routes.
    return localRouteSelector.hasNext()
  }

  /**
   * Return the route from the current connection if it should be retried, even if the connection
   * itself is unhealthy. The biggest gotcha here is that we shouldn't reuse routes from coalesced
   * connections.
   */
  private retryRoute(): Route | null {
    if (this.refusedStreamCount > 1 || this.connectionShutdownCount > 1 || this.otherFailureCount > 0) {
    return null // This route has too many problems to retry.
  }

    let connection = this.call.connection
    if (connection === null || connection === undefined) return null

  // synchronized(connection) {
    if (connection.routeFailureCount != 0) return null
    if (!canReuseConnectionFor(connection.route().address.url,this.address.url)) return null
    return connection.route()
  // }
}

  /**
   * Returns true if the host and port are unchanged from when this was created. This is used to
   * detect if followups need to do a full connection-finding process including DNS resolution, and
   * certificate pin checks.
   */
  sameHostAndPort(url: HttpUrl): boolean {
    let routeUrl = this.address.url
    return url.port == routeUrl.port && url.host == routeUrl.host
  }
}